Skip to content

feat: add raw_json to the webhook event and surface unknown action attempt statuses - #489

Draft
razor-x wants to merge 6 commits into
mainfrom
claude/sdk-webhook-union-resilience-mu1ldf
Draft

feat: add raw_json to the webhook event and surface unknown action attempt statuses#489
razor-x wants to merge 6 commits into
mainfrom
claude/sdk-webhook-union-resilience-mu1ldf

Conversation

@razor-x

@razor-x razor-x commented Sep 1, 2026

Copy link
Copy Markdown
Member

The two rules

1. Runtime does not explode on values the SDK does not recognize. Seam adds event types, action types, error codes, and enum values between releases. Reading them must not raise; writing logic against them is what an upgrade is for.

behavior
unknown event_type falls back to the base Event class
unknown error_code / warning_code falls back to the base error class
unknown enum value reads as itself ($device->device_type === "future_lock")
unknown action attempt status reads as itself

This already held — PHP models enum-typed properties as plain string and only uses the generated enums for tryFrom dispatch, and all 59 discriminated unions carry a default => arm. The tests pin it.

2. The webhook event carries the payload it was parsed from.

$event = $webhook->verify($payload, $headers);
json_decode($event->raw_json())->a_field_this_version_predates;

from_json builds only the properties it was generated for, so a field Seam adds to an existing event is otherwise unreachable. Defined on the base Event, so the variants inherit it and the unrecognized-event fallback answers the same call. Every path reaches events through Event::from_json, so the payload is captured in one place.

Scoped to events — it is there for the verify return, not as a general accessor on every resource, and a test asserts Device does not have it. A method rather than a property because the call is where the serialization happens.

Action attempts

ActionAttemptUnknownStatusError is raised when an attempt reports a status that is neither pending, success, nor error. The resolver previously treated an unrecognized status as non-terminal and polled until the deadline, then reported a timeout that misdescribed what happened. It subclasses ActionAttemptError, so existing handlers for that base keep working.

Not included

Tolerance for malformed payloads. An earlier revision of this branch routed list properties through a helper so that errors: "oops" read as empty instead of raising a TypeError. That is a defect being swallowed rather than forward compatibility — a null list was already read as empty — so it was reverted and array_map restored.

283 tests, 645 assertions. composer lint clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01M2kJ4nGaM8imZCVMEKjmXA

…uses

Seam adds event types, action types, and error codes between SDK releases, so a
payload this version does not recognize should stay readable rather than cost
the caller the whole response.

A list property the API sends as a scalar no longer fails the whole response.
The generated classes mapped these with array_map, which raises a TypeError when
handed anything but an array, so a single unexpected field took down every other
field alongside it. They now route through Seam\Parse::to_list, which reads a
non-list as empty.

Waiting on an action attempt whose status is neither pending, success, nor error
raises the new ActionAttemptUnknownStatusError. The resolver previously treated
an unrecognized status as non-terminal and polled until the deadline, then
reported a timeout that misdescribed what happened. The error subclasses
ActionAttemptError, so existing handlers for that base keep working.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M2kJ4nGaM8imZCVMEKjmXA
The added comments ran to roughly double the density of the code around them and
mostly restated what the line below already said. Kept the ones carrying
information the code cannot: why svix/util has to be required, why both key
shapes are accepted after symbolize_names, and why array_map needed replacing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M2kJ4nGaM8imZCVMEKjmXA
from_json builds only the properties it was generated for, so a field Seam adds
to an existing event between SDK releases is unreachable. SeamWebhook::verify()
now returns an event carrying raw_json():

    json_decode($event->raw_json())->a_field_this_version_predates;

Defined on the base Event, so the variants inherit it and the unrecognized-event
fallback answers the same call. Every path reaches events through
Event::from_json, so the payload is captured in one place.

Scoped to events. It is there for the verify return, not as a general accessor
on every resource. A method rather than a property because the call is where the
serialization happens.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M2kJ4nGaM8imZCVMEKjmXA
Handling values the SDK does not recognize is forward compatibility. Tolerating
payloads that are malformed rather than merely new is a bug being swallowed, and
the two were conflated here.

Removes Seam\Parse and restores array_map for list properties. A null list was
already read as empty, so the guard only caught a list sent as a scalar, which
is a defect worth surfacing rather than silently reading as empty.

Unrecognized handling is untouched: unknown event types still use the base Event
class, unknown error codes the base error class, and unknown enum values read as
themselves. ActionAttemptUnknownStatusError stays, because the action attempt
contract is strict and returning an unrecognized status as a success reports
something the SDK cannot vouch for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M2kJ4nGaM8imZCVMEKjmXA
@razor-x razor-x changed the title Replace array_map with Parse::to_list for robust list parsing feat: add raw_json to the webhook event and surface unknown action attempt statuses Sep 2, 2026
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M2kJ4nGaM8imZCVMEKjmXA
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M2kJ4nGaM8imZCVMEKjmXA
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants